<?php

namespace Itwmw\Attributes\Validate\Provider;

use Itwmw\Attributes\Validate\EventFunc;
use Itwmw\Attributes\Validate\Exception\ValidateTypeException;
use Itwmw\Attributes\Validate\Message;
use Itwmw\Attributes\Validate\Postprocessor;
use Itwmw\Attributes\Validate\Preprocessor;
use Itwmw\Attributes\Validate\Rule;
use Itwmw\Attributes\Validate\RuleMessage;
use Itwmw\Attributes\Validate\Rules\OrRule;
use Itwmw\Attributes\Validate\Rules\RuleInterface;
use Itwmw\Validate\Exception\ValidateException;
use Itwmw\Validate\Exception\ValidateRuntimeException;
use Itwmw\Validate\Support\Concerns\ProcessorInterface;
use Itwmw\Validate\Support\DataAttribute;
use Itwmw\Validate\Support\Processor\DataProcessor;
use Itwmw\Validate\Support\Processor\ProcessorExecCond;
use Itwmw\Validate\Support\Processor\ProcessorOptions;
use Itwmw\Validate\Support\Processor\ProcessorParams;
use Itwmw\Validate\Support\Processor\ProcessorSupport;
use Itwmw\Validate\Support\Rule\BaseRule;
use Itwmw\Validation\Support\Collection\Collection;
use Itwmw\Validation\Support\Rule\OrRule as ValidationOrRule;
use Itwmw\Validation\Support\Str;
use Itwmw\Validation\Support\ValidationException;

/**
 * @template T
 */
class AttributesValidator
{
    /**
     * 验证路径，用于验证失败异常类
     *
     * @var array<string>
     */
    protected array $validatePath = [];

    /**
     * 需要验证的字段
     *
     * @var array<string>
     */
    protected array $validateFields = [];

    /**
     * 判断是否需要验证
     *
     * @param string $field        当前验证的字段
     * @param array  $validatePath 当前的路径
     *
     * @return bool
     */
    protected function needToValidate(string $field, array $validatePath = []): bool
    {
        if (empty($this->validateFields)) {
            return true;
        }

        $validatePathDot  = implode('.', $validatePath);
        $validateFieldDot = empty($validatePathDot) ? $field : $validatePathDot . '.' . $field;
        // 判断是否存在指定的验证字段
        if (in_array($validateFieldDot, $this->validateFields)) {
            return true;
        }

        $validateParentPath = '';
        $haveParentField    = false;
        foreach ($validatePath as $parentPath) {
            $validateParentPath = empty($validateParentPath) ? $parentPath : $validateParentPath . '.' . $parentPath;
            // 判断从自己网上的所有父级是否存在,只要有一层存在，则需要进入下一步判断
            if (in_array($validateParentPath, $this->validateFields)) {
                $haveParentField = true;
                break;
            }
        }

        if ($haveParentField) {
            // 判断上一级父级下是否有其他子字段，如果有，那么当前字段不需要验证
            foreach ($this->validateFields as $checkField) {
                if (str_starts_with($checkField, $validatePathDot . '.')) {
                    return false;
                }
            }
            // 否则需要验证当前路径下的所有字段
            return true;
        } else {
            // 如果所有父级不存在，当前字段也不存在，那么判断是否直接指定了子类型
            foreach ($this->validateFields as $checkField) {
                if (str_starts_with($checkField, $validateFieldDot . '.')) {
                    // 如果指定了其他子类型，那么当前字段需要验证
                    return true;
                }
            }
            // 否则不需要验证
            return false;
        }
    }

    protected function getClassData(object $class, \ReflectionClass $classRef = null): array
    {
        if (is_null($classRef)) {
            $classRef = new \ReflectionClass($class);
        }

        $allProperties = $classRef->getProperties();
        $data          = [];
        foreach ($allProperties as $property) {
            $name = $property->getName();
            if ($property->isInitialized($class)) {
                $data[$name] = $property->getValue($class);
            } else {
                $data[$name] = null;
            }
        }
        return $data;
    }

    protected function setValue(\ReflectionProperty $property, object $class, mixed $value): void
    {
        try {
            $property->setValue($class, $value);
        } catch (\TypeError $e) {
            preg_match_all('/Cannot assign (.*?) to property .*? of type (.*?)$/', $e->getMessage(), $matches);
            throw new ValidateTypeException($matches[2][0], $matches[1][0], implode('.', [...$this->validatePath, $property->getName()]));
        }
    }

    protected function setClassData(object $class, array $data, \ReflectionClass $classRef = null): void
    {
        if (is_null($classRef)) {
            $classRef = new \ReflectionClass($class);
        }

        $allProperties = $classRef->getProperties();
        foreach ($allProperties as $property) {
            $name = $property->getName();
            if (isset($data[$name])) {
                $type = $property->getType();
                if ($type instanceof \ReflectionType && !$type->isBuiltin()) {
                    // 忽略复合类型
                    if ($type instanceof \ReflectionUnionType) {
                        continue;
                    }

                    $className = $type->getName();
                    if ($data[$name] instanceof $className) {
                        $property->setValue($class, $data[$name]);
                        continue;
                    }

                    $subClassRef = new \ReflectionClass($className);
                    $subClass    = $subClassRef->newInstanceWithoutConstructor();
                    $this->setClassData($subClass, $data[$name], $subClassRef);
                    $this->setValue($property, $class, $subClass);
                } else {
                    $this->setValue($property, $class, $data[$name]);
                }
            }
        }
    }

    /**
     * @param object<T>           $class
     * @param \ReflectionClass<T> $classRef
     * @param EventFunc[]         $eventFunc
     * @param Collection          $data
     *
     * @return void
     *
     * @throws ValidateException
     * @throws \ReflectionException
     */
    protected function doEventFunc(object $class, \ReflectionClass $classRef, array $eventFunc, Collection $data): void
    {
        // 使用clone，避免修改验证数据
        foreach ($eventFunc as $func) {
            $method = $func->method;
            if (is_string($method) && method_exists($class, $method)) {
                $refMethod = $classRef->getMethod($func->method);
                $result    = $refMethod->invokeArgs($class, [clone $data, ...$func->getArgs()]);
            } elseif (is_callable($func->method)) {
                $result = call_user_func_array($func->method, [clone $data, ...$func->getArgs()]);
            } else {
                throw new ValidateRuntimeException('Event error or nonexistence');
            }

            if (true !== $result && !is_null($result)) {
                throw new ValidateException($result, 403);
            }
        }
    }

    /**
     * 对字段进行预处理
     *
     * @param object<T>           $class
     * @param \ReflectionClass<T> $classRef
     * @param Collection          $data
     * @param string              $processType
     * @param array               $validatePath
     *
     * @return void
     *
     * @throws \ReflectionException
     */
    protected function doDataProcessing(object $class, \ReflectionClass $classRef, Collection $data, string $processType, array $validatePath = []): void
    {
        $allProperties = $classRef->getProperties();
        foreach ($allProperties as $field) {
            $name = $field->getName();
            if (!$this->needToValidate($name, $validatePath)) {
                continue;
            }
            $type = $field->getType();

            // 从类中获取默认值数据
            if (empty($data->get($name)) && $field->isInitialized($class) && Preprocessor::class === $processType) {
                $data->set($name, $field->getValue($class));
            }

            // 获取字段的处理器并进行处理
            $processors = $this->getProcessor($field, $processType, $classRef, $class);
            if (!empty($processors)) {
                $dataProcessors = $this->processDataCondition($processors);
                if (!is_array($dataProcessors)) {
                    $dataProcessors = [$dataProcessors];
                }
                foreach ($dataProcessors as $dataProcessor) {
                    $this->doProcessing($name, $classRef, $dataProcessor, $data);
                }
            }

            // 处理完后，判断是否为自定义数据结构，如果是，则递归处理
            if ($type instanceof \ReflectionType && !$type->isBuiltin()) {
                if ($type instanceof \ReflectionUnionType) {
                    continue; // 忽略联合类型
                }
                $subClass = $data->get($name);
                // 如果不为空再处理，否则容易出现无限递归
                if (!empty($subClass)) {
                    if (!is_object($subClass)) { // 如果不是对象，则尝试创建一个对象，忽略构造函数
                        $className    = $type->getName();
                        $subClassData = $subClass;
                        $subClassRef  = new \ReflectionClass($className);
                        $subClass     = $subClassRef->newInstanceWithoutConstructor();
                        $this->setClassData($subClass, $subClassData, $subClassRef); // 数据放到类中，之后的验证和后置处理器都会从这里取
                    } else {
                        $subClassRef = new \ReflectionClass($subClass);
                    }
                    $newValidatePath   = $validatePath;
                    $newValidatePath[] = $name;
                    $subCollectData    = validate_collect($this->getClassData($subClass, $subClassRef));
                    $this->doDataProcessing($subClass, $subClassRef, $subCollectData, $processType, $newValidatePath);
                    $this->setClassData($subClass, $subCollectData->all(), $subClassRef);
                    $data->set($name, $subClass);
                }
            }

        }
    }

    /**
     * 验证字段
     *
     * @param object<T>           $class
     * @param \ReflectionClass<T> $classRef
     * @param Collection          $data
     * @param array<string>       $validatePath
     *
     * @return void
     *
     * @throws ValidateException
     * @throws \ReflectionException
     */
    protected function validateAttribute(object $class, \ReflectionClass $classRef, Collection $data, array $validatePath = []): void
    {
        $allProperties = $classRef->getProperties();
        foreach ($allProperties as $property) {
            $name = $property->getName();
            if (!$this->needToValidate($name, $validatePath)) {
                continue;
            }

            $type = $property->getType();
            $this->validateField($class, $property, $data);

            // 处理完后，判断是否为自定义数据结构，如果是，则递归处理
            if ($type instanceof \ReflectionType && !$type->isBuiltin()) {
                if ($type instanceof \ReflectionUnionType) {
                    continue; // 忽略联合类型
                }

                $subClass = $data->get($name);
                // 如果不为空再处理，否则容易出现无限递归
                if (!empty($subClass)) {
                    $subCollectData     = validate_collect($this->getClassData($subClass));
                    $newValidatePath    = $validatePath;
                    $newValidatePath[]  = $name;
                    $this->validatePath = $newValidatePath;
                    $this->validateAttribute($subClass, new \ReflectionClass($subClass), $subCollectData, $newValidatePath);
                }
            }
        }
    }

    protected function init(): void
    {
        $this->validatePath = [];
    }

    protected function getNeedCheckData(array $data, array $fields): array
    {
        $checkData = [];

        foreach ($fields as $field) {
            $haveSubField = false;
            foreach ($fields as $f) {
                // 判断是否有子字段，如果有，则不获取当前字段的所有值，而是选择获取子字段
                if (str_starts_with($f, $field . '.')) {
                    $haveSubField = true;
                    break;
                }
            }
            if (!$haveSubField) {
                data_set($checkData, $field, data_get($data, $field) ?? null);
            }

        }
        return $checkData;
    }

    protected function scene(object $class, string $sceneName): bool|array
    {
        $sceneFunc = 'scene' . ucfirst($sceneName);
        if (method_exists($class, $sceneFunc)) {
            $result = call_user_func([$class, $sceneFunc]);
            if (is_array($result)) {
                return $result;
            }
        }

        return false;
    }

    /**
     * @param class-string<T>|object<T> $class
     * @param array                     $data
     * @param array                     $fields
     * @param array<EventFunc>          $before
     * @param array<EventFunc>          $after
     * @param string                    $scene
     *
     * @return object<T>
     *
     * @throws ValidateException
     * @throws \ReflectionException
     */
    public function validate(string|object $class, array $data = [], array $fields = [], array $before = [], array $after = [], string $scene = ''): object
    {
        $this->init();
        $classRef = new \ReflectionClass($class);
        if (is_string($class)) {
            $class = $classRef->newInstanceWithoutConstructor();
        }

        if (!empty($scene)) {
            if (($result = $this->scene($class, $scene)) !== false) {
                $fields = $result;
            }
        }

        if (empty($data)) {
            $data = $this->getClassData($class, $classRef);
        }

        if (!empty($fields)) {
            $checkData = $this->getNeedCheckData($data, $fields);
        } else {
            $checkData = $data;
        }

        $checkData            = validate_collect($checkData);
        $this->validateFields = $fields;
        if (!empty($before)) {
            $this->doEventFunc($class, $classRef, $before, $checkData); // 触发前置事件
        }

        $this->doDataProcessing($class, $classRef, $checkData, Preprocessor::class);
        $this->validateAttribute($class, $classRef, $checkData);
        $this->doDataProcessing($class, $classRef, $checkData, Postprocessor::class);
        if (!empty($after)) {
            $this->doEventFunc($class, $classRef, $after, $checkData); // 触发后置事件
        }
        $this->setClassData($class, $checkData->all(), $classRef);

        return $class;
    }

    protected function addDefaultRuleForType(\ReflectionNamedType $type, array $rule): array
    {
        $typeRule = [
            'string', 'integer', 'boolean', 'numeric', 'decimal'
        ];

        if (!in_array('nullable', $rule) && $type->allowsNull()) {
            $rule[] = 'nullable';
        }

        if (!$type->isBuiltin()) {
            return $rule;
        }

        if (!empty(array_intersect($typeRule, array_filter($rule, fn ($item) => is_string($item))))) {
            return $rule;
        }

        $typeName = $type->getName();
        switch (true) {
            case 'int' === $typeName:
                if (!in_array('integer', $rule)) {
                    $rule[] = 'integer';
                }
                if (!in_array('numeric', $rule)) {
                    $rule[] = 'numeric';
                }
                break;
            case 'float' === $typeName && !in_array('numeric', $rule):
                $rule[] = 'numeric';
                break;
            case 'string' === $typeName && !in_array('string', $rule):
                $rule[] = 'string';
                break;
            case 'bool' === $typeName && !in_array('boolean', $rule):
                $rule[] = 'boolean';
                break;
        }
        return $rule;
    }

    /**
     * 验证字段
     *
     * @param object<T>                  $class
     * @param string|\ReflectionProperty $field
     * @param Collection                 $data
     *
     * @return void
     *
     * @throws ValidateException
     * @throws \ReflectionException
     */
    protected function validateField(object $class, string|\ReflectionProperty $field, Collection $data): void
    {
        if (!($field instanceof \ReflectionProperty)) {
            $field = new \ReflectionProperty($class, $field);
        }
        $type = $field->getType();

        $subRules             = [];
        $extendRules          = [];
        $extendImplicitRules  = [];
        $extendDependentRules = [];
        $customAttributes     = [];
        $messages             = [];
        $ruleMessages         = [];

        // 获取字段的内置验证规则
        $validateRules = $field->getAttributes(RuleInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
        foreach ($validateRules as $rule) {
            $subRules[] = $this->parseRule($rule->newInstance());
        }
        // 获取字段的自定义验证规则
        $customRules = $field->getAttributes(Rule::class, \ReflectionAttribute::IS_INSTANCEOF);
        foreach ($customRules as $customRule) {
            $customRule = $customRule->newInstance();
            // 判断如果是当前类的方法实现的规则，则进行规则注入
            /** @var Rule $customRule */
            $ruleName = $customRule->name;
            $args     = $customRule->args ?? [];

            if (class_exists($ruleName) && is_subclass_of($ruleName, BaseRule::class)) {
                $ruleClass  = !is_null($args) ? new $ruleName(...$args) : new $ruleName();
                $subRules[] = $ruleClass;
                continue;
            } else {
                $subRules[] = $customRule->getRule();
            }

            $extension = null;

            if (is_string($ruleName) && method_exists($class, $ruleName)) {
                $customRuleMethod  = new \ReflectionMethod($class, $customRule->name);
                $extension         = $customRuleMethod->getClosure($class);
                $customRuleMessage = $customRuleMethod->getAttributes(RuleMessage::class, \ReflectionAttribute::IS_INSTANCEOF);
                if (!empty($customRuleMessage)) {
                    $customRuleMessage                           = reset($customRuleMessage)->newInstance();
                    $ruleMessages[Str::snake($customRule->name)] = $customRuleMessage->getMessage();
                }
            }

            // 通过闭包实现的验证规则
            if (is_callable($ruleName)) {
                $extension = $ruleName;
            }

            if (is_null($extension)) {
                throw new \RuntimeException('自定义规则必须是当前类的方法或者是可调用的');
            }

            if (Rule::TYPE_NORMAL === $customRule->type) {
                $extendRules[$ruleName] = $extension;
            } elseif (Rule::TYPE_IMPLICIT === $customRule->type) {
                $extendImplicitRules[$ruleName] = $extension;
            } elseif (Rule::TYPE_DEPENDENT === $customRule->type) {
                $extendDependentRules[$ruleName] = $extension;
            } else {
                throw new \RuntimeException('未知的自定义规则类型');
            }

        }
        if ($type instanceof \ReflectionNamedType) {
            $subRules = $this->addDefaultRuleForType($type, $subRules);
        }

        // 获取字段的自定义错误消息
        $name            = $field->getName();
        $validateMessage = $field->getAttributes(Message::class, \ReflectionAttribute::IS_INSTANCEOF);
        if (!empty($validateMessage)) {
            $messages[$name] = [];
            /** @var Message $validateMessage */
            $validateMessage         = $validateMessage[0]->newInstance();
            $customAttributes[$name] = $validateMessage->getName();
            foreach ($validateMessage->getMessages() as $ruleNameClass => $message) {
                $messages[$name][lcfirst(basename($ruleNameClass))] = $message;
            }
        }

        $validator = InternalValidator::make($data->all(), [$name => $subRules], $messages, $customAttributes);
        $validator->addExtensions($extendRules);
        $validator->addImplicitExtensions($extendImplicitRules);
        $validator->addDependentExtensions($extendDependentRules);
        $validator->setFallbackMessages($ruleMessages);

        try {
            $validator->newPasses($name);
        } catch (ValidationException $e) {
            throw $this->getException($e, $messages, $customAttributes, $data->all());
        }
    }

    protected function getException(ValidationException $e, array $message, array $customAttributes, array $data): ValidateException
    {
        $attribute = $e->getAttribute();
        $rule      = $e->getRule();

        $messageProvider = new MessageProvider();
        $messageProvider->setMessages($message);
        $messageProvider->setCustomAttributes($customAttributes);
        $messageProvider->setData($data);
        $messageProvider->setValidateFieldPath($this->validatePath);
        $messageProvider->setAttribute($attribute)->setRule($rule);

        $error = $messageProvider->handleMessage($e->getMessage());

        return new ValidateException($error, 403, implode('.', [...$this->validatePath, $attribute]), $e);
    }

    protected function getProcessor(\ReflectionProperty $property, string $type, \ReflectionClass $classRef, object $class): array
    {
        $preprocessors      = [];
        $validateProcessors = $property->getAttributes($type);
        if (!empty($validateProcessors)) {
            foreach ($validateProcessors as $validateProcessor) {
                $validateProcessor = $validateProcessor->newInstance();
                /** @var Preprocessor $validateProcessor */
                $handler = $validateProcessor->getHandler();
                if (is_string($handler) && !is_callable($handler)) {
                    if (method_exists($class, $handler)) {
                        $handler = fn (...$params) => $classRef->getMethod($handler)->invokeArgs($class, $params);
                    }
                }
                $params = $validateProcessor->getParams();
                if (!empty($params)) {
                    $processor = [$handler, ...$params];
                } else {
                    $processor = $handler;
                }
                $preprocessors[] = $processor;
            }
            if (1 === count($preprocessors)) {
                $preprocessors = $preprocessors[0];
            } else {
                $preprocessors[] = ProcessorOptions::MULTIPLE;
            }
        }
        return $preprocessors;
    }

    protected function parseRule(RuleInterface $rule): string|ValidationOrRule
    {
        if ($rule instanceof OrRule) {
            return $this->parseOrRule($rule);
        }
        $ruleName = basename(get_class($rule));
        if (str_ends_with($ruleName, 'Rule')) {
            $ruleName = substr($ruleName, 0, -4);
        }

        $ruleName = lcfirst($ruleName);

        $ruleArgs = implode(',', $rule->getArgs());
        if (0 === strlen($ruleArgs)) {
            $ruleString = $ruleName;
        } else {
            $ruleString = "{$ruleName}:{$ruleArgs}";
        }

        return $ruleString;
    }

    protected function parseOrRule(OrRule $rule): ValidationOrRule
    {
        $rulesString = [];
        foreach ($rule->subRules as $subRule) {
            if (is_subclass_of($subRule, RuleInterface::class)) {
                if (is_string($subRule)) {
                    $subRule = new $subRule();
                }

                $rulesString[] = $this->parseRule($subRule);
            }
        }
        return new ValidationOrRule($rulesString);
    }

    protected function doProcessing(
        string $field,
        object $class,
        DataProcessor $dataProcessor,
        Collection $data
    ): void {
        $isEmpty = function ($value) {
            return null === $value || [] === $value || '' === $value;
        };

        $value         = $data->get($field);
        $dataAttribute = new DataAttribute();

        if (ProcessorExecCond::WHEN_EMPTY === $dataProcessor->execCond && !$isEmpty($value)) {
            $data->set($field, $value);
            return;
        } elseif (ProcessorExecCond::WHEN_NOT_EMPTY === $dataProcessor->execCond && $isEmpty($value)) {
            $data->set($field, $value);
            return;
        }

        $params   = $dataProcessor->params;
        $callback = $dataProcessor->callback;

        if (empty($params)) {
            $callParams = [$value, $field, $data->all(), $dataAttribute];
        } else {
            $callParams = array_map(function ($param) use ($value, $field, $data, $dataAttribute) {
                return match ($param) {
                    ProcessorParams::Value         => $value,
                    ProcessorParams::Attribute     => $field,
                    ProcessorParams::OriginalData  => $data->all(),
                    ProcessorParams::DataAttribute => $dataAttribute
                };
            }, $params);
        }

        if (is_callable($callback)) {
            $value = call_user_func($callback, ...$callParams);
        } elseif (
            (is_string($callback) || is_object($callback))
            && class_exists($callback)
            && is_subclass_of($callback, ProcessorInterface::class)
        ) {
            /** @var ProcessorInterface $callback */
            $callback = new $callback($dataAttribute);
            $value    = $callback->handle($value, $field, $data->all());
        } elseif (is_string($callback) && method_exists($class, lcfirst($callback) . 'Processor')) {
            $value = call_user_func([$class,  lcfirst($callback) . 'Processor'], ...$callParams);
        } else {
            $value = $callback;
        }

        if (true === $dataAttribute->deleteField) {
            $data->forget($field);
        } else {
            $data->set($field, $value);
        }
    }

    protected function processDataCondition(mixed $callback): DataProcessor|array
    {
        $execCond       = ProcessorExecCond::ALWAYS;
        $params         = [];
        $dataProcessors = [];
        $dataProcessor  = new DataProcessor();
        if (is_array($callback) && !empty($callback)) {
            $valueIsArray = false;

            if (in_array(ProcessorOptions::MULTIPLE, $callback)) {
                foreach ($callback as $item) {
                    if ($item instanceof ProcessorSupport) {
                        continue;
                    }
                    $dataProcessor    = $this->processDataCondition($item);
                    $dataProcessors[] = $dataProcessor;
                }
                return $dataProcessors;
            }

            $callback = array_filter($callback, function ($item) use (&$params, &$valueIsArray, &$execCond) {
                if ($item instanceof ProcessorExecCond) {
                    $execCond = $item;
                } elseif ($item instanceof ProcessorParams) {
                    $params[] = $item;
                    return false;
                } elseif ($item instanceof ProcessorSupport) {
                    if (ProcessorOptions::VALUE_IS_ARRAY === $item) {
                        $valueIsArray = true;
                    }
                    return false;
                }

                return true;
            });

            if (!$valueIsArray) {
                if (ProcessorExecCond::ALWAYS !== $execCond || !empty($params)) {
                    $callback = $callback[0];
                }
            }
        }

        $dataProcessor->execCond = $execCond;
        $dataProcessor->params   = $params;
        $dataProcessor->callback = $callback;
        return $dataProcessor;
    }
}
